package cn.chengpengper.rpc.proxy.api.object;

import cn.chengpengper.rpc.cache.result.CacheResultKey;
import cn.chengpengper.rpc.cache.result.CacheResultManager;
import cn.chengpengper.rpc.constants.RpcConstants;
import cn.chengpengper.rpc.protocol.RpcProtocol;
import cn.chengpengper.rpc.protocol.enums.RpcType;
import cn.chengpengper.rpc.protocol.header.RpcHeaderFactory;
import cn.chengpengper.rpc.protocol.request.RpcRequest;
import cn.chengpengper.rpc.proxy.api.async.IAsyncObjectProxy;
import cn.chengpengper.rpc.proxy.api.consumer.Consumer;
import cn.chengpengper.rpc.proxy.api.future.RpcFuture;
import cn.chengpengper.rpc.registry.api.RegistryService;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author chengpeng.hu
 */
@Slf4j
public class ObjectProxy<T> implements InvocationHandler, IAsyncObjectProxy {

    private Class<T> clazz;

    private String serviceVersion;

    private String serviceGroup;

    private long timeout = 15000;

    private Consumer consumer;

    private String serializationType;

    private boolean async;

    private boolean oneway;

    private RegistryService registryService;

    private boolean enableResultCache;

    private CacheResultManager<Object> cacheResultManager;

    public ObjectProxy(Class<T> clazz) {
        this.clazz = clazz;
    }

    public ObjectProxy(Class<T> clazz, String serviceVersion, String serviceGroup, String serializationType, long timeout, RegistryService registryService, Consumer consumer, boolean async, boolean oneway, boolean enableResultCache, int resultCacheExpire) {
        this.clazz = clazz;
        this.serviceVersion = serviceVersion;
        this.timeout = timeout;
        this.serviceGroup = serviceGroup;
        this.consumer = consumer;
        this.serializationType = serializationType;
        this.async = async;
        this.oneway = oneway;
        this.registryService = registryService;
        this.enableResultCache = enableResultCache;
        if (resultCacheExpire <= 0) {
            resultCacheExpire = RpcConstants.RPC_SCAN_RESULT_CACHE_EXPIRE;
        }
        this.cacheResultManager = CacheResultManager.getInstance(resultCacheExpire, enableResultCache);

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class == method.getDeclaringClass()) {
            String methodName = method.getName();
            if ("equals".equals(methodName)) {
                return proxy == args[0];
            }
            if ("hashCode".equals(methodName)) {
                return System.identityHashCode(proxy);
            }
            if ("toString".equals(methodName)) {
                return proxy.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(proxy)) + ", with InvocationHandler " + this;
            }
            throw new IllegalStateException(method.getName());
        }

        if (enableResultCache) {
            return invokeSendRequestMethodCache(method, args);
        }
        return invokeSendRequestMethod(method, args);
    }

    private Object invokeSendRequestMethodCache(Method method, Object[] args) throws Exception {
        CacheResultKey cacheResultKey = new CacheResultKey(method.getDeclaringClass().getName(), method.getName(), method.getParameterTypes(), args, this.serviceVersion, this.serviceGroup);
        Object obj = this.cacheResultManager.get(cacheResultKey);
        if (Objects.isNull(obj)) {
            obj = invokeSendRequestMethod(method, args);
            if (Objects.nonNull(obj)) {
                cacheResultKey.setCacheTimeStamp(System.currentTimeMillis());
                this.cacheResultManager.put(cacheResultKey, obj);
            }
        } else {
            log.info("from consumer cache");
        }
        return obj;
    }

    /**
     * 调用真正的请求方法
     *
     * @param method
     * @param args
     * @return
     * @throws Exception
     */
    private Object invokeSendRequestMethod(Method method, Object[] args) throws Exception {
        RpcProtocol<RpcRequest> protocol = new RpcProtocol<>();
        protocol.setHeader(RpcHeaderFactory.getRequestHeader(serializationType, RpcType.REQUEST));

        RpcRequest request = new RpcRequest();
        request.setVersion(this.serviceVersion);
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParameterTypes(method.getParameterTypes());
        request.setGroup(this.serviceGroup);
        request.setParameters(args);
        request.setAsync(async);
        request.setOneway(oneway);

        protocol.setBody(request);

        // Debug
        log.debug(method.getDeclaringClass().getName());
        log.debug(method.getName());

        if (method.getParameterTypes().length > 0) {
            for (int i = 0; i < method.getParameterTypes().length; ++i) {
                log.debug(method.getParameterTypes()[i].getName());
            }
        }

        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; ++i) {
                log.debug(args[i].toString());
            }
        }

        RpcFuture rpcFuture = this.consumer.sendRequest(protocol, registryService);
        return rpcFuture == null ? null : timeout > 0 ? rpcFuture.get(timeout, TimeUnit.MILLISECONDS) : rpcFuture.get();
    }

    @Override
    public RpcFuture call(String funcName, Object... args) {
        RpcProtocol<RpcRequest> request = createRequest(this.clazz.getName(), funcName, args);
        RpcFuture rpcFuture = null;
        try {
            rpcFuture = this.consumer.sendRequest(request, registryService);
        } catch (Exception e) {
            log.error("async all throws exception:", e);
        }
        return rpcFuture;
    }

    private RpcProtocol<RpcRequest> createRequest(String className, String methodName, Object[] args) {

        RpcProtocol<RpcRequest> requestRpcProtocol = new RpcProtocol<RpcRequest>();

        requestRpcProtocol.setHeader(RpcHeaderFactory.getRequestHeader(serializationType, RpcType.REQUEST));

        RpcRequest request = new RpcRequest();
        request.setClassName(className);
        request.setMethodName(methodName);
        request.setParameters(args);
        request.setVersion(this.serviceVersion);
        request.setGroup(this.serviceGroup);

        Class[] parameterTypes = new Class[args.length];
        // Get the right class type
        for (int i = 0; i < args.length; i++) {
            parameterTypes[i] = getClassType(args[i]);
        }
        request.setParameterTypes(parameterTypes);
        requestRpcProtocol.setBody(request);

        log.debug(className);
        log.debug(methodName);
        for (int i = 0; i < parameterTypes.length; ++i) {
            log.debug(parameterTypes[i].getName());
        }
        for (int i = 0; i < args.length; ++i) {
            log.debug(args[i].toString());
        }

        return requestRpcProtocol;
    }

    private Class<?> getClassType(Object obj) {
        Class<?> classType = obj.getClass();
        String typeName = classType.getName();
        switch (typeName) {
            case "java.lang.Integer":
                return Integer.TYPE;
            case "java.lang.Long":
                return Long.TYPE;
            case "java.lang.Float":
                return Float.TYPE;
            case "java.lang.Double":
                return Double.TYPE;
            case "java.lang.Character":
                return Character.TYPE;
            case "java.lang.Boolean":
                return Boolean.TYPE;
            case "java.lang.Short":
                return Short.TYPE;
            case "java.lang.Byte":
                return Byte.TYPE;
            default:
                return classType;
        }
    }

}
